package io.kwdb.sdk.internal;

import io.kwdb.sdk.Status;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Retry policy for handling transient errors with exponential backoff and jitter. */
public final class RetryPolicy {

  private static final Logger logger = Logger.getLogger(RetryPolicy.class.getName());
  private static final Random random = new Random();

  private final boolean enableRetry;
  private final int maxRetries;
  private final long baseBackoffMs;
  private final long maxBackoffMs;

  /**
   * Create a retry policy with specified configuration.
   *
   * @param enableRetry Whether to enable retry
   * @param maxRetries Maximum number of retry attempts (0-10)
   * @param baseBackoffMs Base backoff time in milliseconds
   */
  public RetryPolicy(boolean enableRetry, int maxRetries, long baseBackoffMs) {
    this.enableRetry = enableRetry;
    this.maxRetries = Math.min(Math.max(0, maxRetries), 10);
    this.baseBackoffMs = Math.max(1, baseBackoffMs);
    this.maxBackoffMs = 2000; // Cap at 2 seconds
  }

  /**
   * Execute an operation with retry logic.
   *
   * @param operation The operation to execute
   * @return The final status after retries
   */
  public Status execute(Callable<Status> operation) {
    if (!enableRetry) {
      try {
        return operation.call();
      } catch (Exception e) {
        return Status.internal("Operation failed: " + e.getMessage());
      }
    }

    long backoff = baseBackoffMs;
    Exception lastException = null;

    for (int attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        Status status = operation.call();

        if (status.isOk() || !isTransient(status)) {
          // Success or non-transient error - don't retry
          if (attempt > 0 && status.isOk()) {
            logger.log(Level.INFO, "Operation succeeded after {0} retries", attempt);
          }
          return status;
        }

        // Transient error - check if we should retry
        if (attempt < maxRetries) {
          long sleepMs = calculateBackoff(backoff, attempt);
          logger.log(
              Level.INFO,
              "Retrying after {0}ms (attempt {1}/{2}), error: {3}",
              new Object[] {sleepMs, attempt + 1, maxRetries, status.getMessage()});

          Thread.sleep(sleepMs);
          backoff = Math.min(backoff * 2, maxBackoffMs);
        } else {
          // Max retries exceeded
          logger.log(
              Level.WARNING,
              "Max retries ({0}) exceeded, last error: {1}",
              new Object[] {maxRetries, status.getMessage()});
          return status;
        }

      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return Status.cancelled("Operation interrupted");
      } catch (Exception e) {
        lastException = e;
        logger.log(Level.WARNING, "Exception during operation attempt " + attempt, e);

        if (attempt < maxRetries) {
          long sleepMs = calculateBackoff(backoff, attempt);
          try {
            Thread.sleep(sleepMs);
            backoff = Math.min(backoff * 2, maxBackoffMs);
          } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            return Status.cancelled("Retry interrupted");
          }
        }
      }
    }

    // If we get here, all retries failed
    String message =
        lastException != null
            ? "All retries failed: " + lastException.getMessage()
            : "All retries failed";
    return Status.unavailable(message);
  }

  /**
   * Check if an error is transient and should be retried. Only network/timeout errors are
   * considered transient.
   */
  private boolean isTransient(Status status) {
    // Transient errors that should be retried:
    // - UNAVAILABLE (network issues)
    // - DEADLINE_EXCEEDED (timeout)
    // - RESOURCE_EXHAUSTED (temporary overload)
    // - ABORTED (conflict, can retry)

    switch (status.getCode()) {
      case UNAVAILABLE:
      case DEADLINE_EXCEEDED:
      case RESOURCE_EXHAUSTED:
      case ABORTED:
        return true;
      default:
        return false;
    }
  }

  /** Calculate backoff time with jitter. */
  private long calculateBackoff(long baseBackoff, int attempt) {
    // Add jitter: ±25% of base backoff
    long jitter = (long) (baseBackoff * 0.25 * (random.nextDouble() * 2 - 1));
    return Math.max(1, baseBackoff + jitter);
  }

  /** Create a default retry policy. */
  public static RetryPolicy defaultPolicy() {
    return new RetryPolicy(true, 3, 100);
  }

  /** Create a no-retry policy. */
  public static RetryPolicy noRetry() {
    return new RetryPolicy(false, 0, 0);
  }
}
